rkerberos 0.2.2 → 0.3.0
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 +4 -4
- data/CHANGES.md +22 -0
- data/EXAMPLES.md +1015 -0
- data/MANIFEST.md +17 -12
- data/README.md +5 -5
- data/ext/rkerberos/ccache.c +105 -26
- data/ext/rkerberos/config.c +59 -10
- data/ext/rkerberos/context.c +76 -7
- data/ext/rkerberos/extconf.rb +16 -3
- data/ext/rkerberos/kadm5.c +301 -96
- data/ext/rkerberos/keytab.c +222 -66
- data/ext/rkerberos/keytab_entry.c +2 -1
- data/ext/rkerberos/policy.c +25 -23
- data/ext/rkerberos/principal.c +136 -10
- data/ext/rkerberos/rkerberos.c +244 -34
- data/ext/rkerberos/rkerberos.h +8 -0
- data/rkerberos.gemspec +10 -7
- data/spec/config_spec.rb +58 -3
- data/spec/context_spec.rb +57 -7
- data/spec/credentials_cache_spec.rb +106 -39
- data/spec/kadm5_spec.rb +292 -17
- data/spec/krb5_keytab_spec.rb +229 -24
- data/spec/krb5_spec.rb +142 -29
- data/spec/policy_spec.rb +12 -7
- data/spec/principal_spec.rb +102 -4
- data/spec/spec_helper.rb +34 -0
- metadata +10 -8
data/spec/krb5_keytab_spec.rb
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
require 'rkerberos'
|
|
5
5
|
require 'tmpdir'
|
|
6
6
|
require 'fileutils'
|
|
7
|
-
require 'pty'
|
|
8
|
-
require 'expect'
|
|
9
|
-
|
|
10
7
|
|
|
8
|
+
unless File::ALT_SEPARATOR
|
|
9
|
+
require 'pty'
|
|
10
|
+
require 'expect'
|
|
11
|
+
end
|
|
11
12
|
|
|
12
|
-
RSpec.describe Kerberos::Krb5::Keytab do
|
|
13
|
+
RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
|
|
13
14
|
before(:all) do
|
|
14
15
|
@realm = Kerberos::Kadm5::Config.new.realm
|
|
15
16
|
@keytab_file = File.join(Dir.tmpdir, 'test.keytab')
|
|
@@ -36,21 +37,49 @@ RSpec.describe Kerberos::Krb5::Keytab do
|
|
|
36
37
|
subject(:keytab) { described_class.new }
|
|
37
38
|
|
|
38
39
|
describe 'constructor' do
|
|
39
|
-
it 'accepts an optional name' do
|
|
40
|
-
expect { described_class.new("FILE:/usr/local/var/keytab") }.not_to raise_error
|
|
41
|
-
expect { described_class.new("FILE:/bogus/keytab") }.not_to raise_error
|
|
40
|
+
it 'accepts an optional name keyword' do
|
|
41
|
+
expect { described_class.new(name: "FILE:/usr/local/var/keytab") }.not_to raise_error
|
|
42
|
+
expect { described_class.new(name: "FILE:/bogus/keytab") }.not_to raise_error
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
it 'raises error for invalid residual type' do
|
|
45
46
|
expect {
|
|
46
|
-
described_class.new("BOGUS:/tmp/keytab")
|
|
47
|
+
described_class.new(name: "BOGUS:/tmp/keytab")
|
|
47
48
|
}.to raise_error(Kerberos::Krb5::Keytab::Exception)
|
|
48
49
|
end
|
|
50
|
+
|
|
51
|
+
it 'accepts a context keyword argument' do
|
|
52
|
+
ctx = Kerberos::Krb5::Context.new
|
|
53
|
+
expect { described_class.new(name: @keytab_name, context: ctx) }.not_to raise_error
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'works with context and no name' do
|
|
57
|
+
ctx = Kerberos::Krb5::Context.new
|
|
58
|
+
expect { described_class.new(context: ctx) }.not_to raise_error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'rejects positional arguments' do
|
|
62
|
+
expect { described_class.new(@keytab_name) }.to raise_error(ArgumentError)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'rejects unknown keywords' do
|
|
66
|
+
expect { described_class.new(unknown: true) }.to raise_error(ArgumentError, /unknown keyword/)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'raises TypeError for non-Context context argument' do
|
|
70
|
+
expect { described_class.new(context: "bad") }.to raise_error(TypeError)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'raises error for a closed context' do
|
|
74
|
+
ctx = Kerberos::Krb5::Context.new
|
|
75
|
+
ctx.close
|
|
76
|
+
expect { described_class.new(context: ctx) }.to raise_error(Kerberos::Krb5::Exception)
|
|
77
|
+
end
|
|
49
78
|
end
|
|
50
79
|
|
|
51
80
|
describe '#keytab_name and #keytab_type' do
|
|
52
81
|
it 'returns the underlying name and type strings' do
|
|
53
|
-
kt = described_class.new(@keytab_name)
|
|
82
|
+
kt = described_class.new(name: @keytab_name)
|
|
54
83
|
expect(kt).to respond_to(:keytab_name)
|
|
55
84
|
expect(kt).to respond_to(:keytab_type)
|
|
56
85
|
|
|
@@ -66,30 +95,30 @@ RSpec.describe Kerberos::Krb5::Keytab do
|
|
|
66
95
|
|
|
67
96
|
describe '#close' do
|
|
68
97
|
it 'returns true' do
|
|
69
|
-
kt = described_class.new(@keytab_name)
|
|
98
|
+
kt = described_class.new(name: @keytab_name)
|
|
70
99
|
expect(kt.close).to eq(true)
|
|
71
100
|
end
|
|
72
101
|
|
|
73
102
|
it 'can be called multiple times without error' do
|
|
74
|
-
kt = described_class.new(@keytab_name)
|
|
103
|
+
kt = described_class.new(name: @keytab_name)
|
|
75
104
|
kt.close
|
|
76
105
|
expect { kt.close }.not_to raise_error
|
|
77
106
|
end
|
|
78
107
|
|
|
79
108
|
it 'raises an error when calling keytab_name after close' do
|
|
80
|
-
kt = described_class.new(@keytab_name)
|
|
109
|
+
kt = described_class.new(name: @keytab_name)
|
|
81
110
|
kt.close
|
|
82
111
|
expect { kt.keytab_name }.to raise_error(Kerberos::Krb5::Exception)
|
|
83
112
|
end
|
|
84
113
|
|
|
85
114
|
it 'raises an error when calling keytab_type after close' do
|
|
86
|
-
kt = described_class.new(@keytab_name)
|
|
115
|
+
kt = described_class.new(name: @keytab_name)
|
|
87
116
|
kt.close
|
|
88
117
|
expect { kt.keytab_type }.to raise_error(Kerberos::Krb5::Exception)
|
|
89
118
|
end
|
|
90
119
|
|
|
91
120
|
it 'does not segfault when garbage collected after close' do
|
|
92
|
-
kt = described_class.new(@keytab_name)
|
|
121
|
+
kt = described_class.new(name: @keytab_name)
|
|
93
122
|
kt.close
|
|
94
123
|
kt = nil
|
|
95
124
|
GC.start
|
|
@@ -151,7 +180,7 @@ RSpec.describe Kerberos::Krb5::Keytab do
|
|
|
151
180
|
|
|
152
181
|
describe '#each' do
|
|
153
182
|
it 'yields keytab entries for a valid keytab' do
|
|
154
|
-
kt = described_class.new(@keytab_name)
|
|
183
|
+
kt = described_class.new(name: @keytab_name)
|
|
155
184
|
entries = []
|
|
156
185
|
kt.each { |entry| entries << entry }
|
|
157
186
|
expect(entries.length).to eq(2)
|
|
@@ -165,14 +194,14 @@ RSpec.describe Kerberos::Krb5::Keytab do
|
|
|
165
194
|
end
|
|
166
195
|
|
|
167
196
|
it 'does not leak resources when the block raises' do
|
|
168
|
-
kt = described_class.new(@keytab_name)
|
|
197
|
+
kt = described_class.new(name: @keytab_name)
|
|
169
198
|
expect {
|
|
170
199
|
kt.each { |_| raise "boom" }
|
|
171
200
|
}.to raise_error(RuntimeError, "boom")
|
|
172
201
|
end
|
|
173
202
|
|
|
174
203
|
it 'does not leak resources when the block breaks' do
|
|
175
|
-
kt = described_class.new(@keytab_name)
|
|
204
|
+
kt = described_class.new(name: @keytab_name)
|
|
176
205
|
result = catch(:done) do
|
|
177
206
|
kt.each { |_| throw :done, :escaped }
|
|
178
207
|
:completed
|
|
@@ -183,7 +212,7 @@ RSpec.describe Kerberos::Krb5::Keytab do
|
|
|
183
212
|
|
|
184
213
|
describe '#get_entry' do
|
|
185
214
|
it 'finds an entry by principal name' do
|
|
186
|
-
kt = described_class.new(@keytab_name)
|
|
215
|
+
kt = described_class.new(name: @keytab_name)
|
|
187
216
|
entry = kt.get_entry("testuser1@#{@realm}")
|
|
188
217
|
expect(entry).to be_a(Kerberos::Krb5::Keytab::Entry)
|
|
189
218
|
expect(entry.principal).to eq("testuser1@#{@realm}")
|
|
@@ -193,14 +222,14 @@ RSpec.describe Kerberos::Krb5::Keytab do
|
|
|
193
222
|
end
|
|
194
223
|
|
|
195
224
|
it 'finds an entry filtering by vno' do
|
|
196
|
-
kt = described_class.new(@keytab_name)
|
|
225
|
+
kt = described_class.new(name: @keytab_name)
|
|
197
226
|
entry = kt.get_entry("testuser1@#{@realm}", 1)
|
|
198
227
|
expect(entry.principal).to eq("testuser1@#{@realm}")
|
|
199
228
|
expect(entry.vno).to eq(1)
|
|
200
229
|
end
|
|
201
230
|
|
|
202
231
|
it 'finds an entry filtering by vno and enctype' do
|
|
203
|
-
kt = described_class.new(@keytab_name)
|
|
232
|
+
kt = described_class.new(name: @keytab_name)
|
|
204
233
|
# aes128-cts-hmac-sha1-96 is enctype 17
|
|
205
234
|
entry = kt.get_entry("testuser1@#{@realm}", 1, 17)
|
|
206
235
|
expect(entry.principal).to eq("testuser1@#{@realm}")
|
|
@@ -209,21 +238,21 @@ RSpec.describe Kerberos::Krb5::Keytab do
|
|
|
209
238
|
end
|
|
210
239
|
|
|
211
240
|
it 'raises an error for a non-existent principal' do
|
|
212
|
-
kt = described_class.new(@keytab_name)
|
|
241
|
+
kt = described_class.new(name: @keytab_name)
|
|
213
242
|
expect {
|
|
214
243
|
kt.get_entry("bogus@#{@realm}")
|
|
215
244
|
}.to raise_error(Kerberos::Krb5::Exception)
|
|
216
245
|
end
|
|
217
246
|
|
|
218
247
|
it 'is aliased as find' do
|
|
219
|
-
kt = described_class.new(@keytab_name)
|
|
248
|
+
kt = described_class.new(name: @keytab_name)
|
|
220
249
|
expect(kt.method(:find)).to eq(kt.method(:get_entry))
|
|
221
250
|
end
|
|
222
251
|
end
|
|
223
252
|
|
|
224
253
|
describe '#dup' do
|
|
225
254
|
it 'creates an independent handle referring to same keytab' do
|
|
226
|
-
kt1 = described_class.new(@keytab_name)
|
|
255
|
+
kt1 = described_class.new(name: @keytab_name)
|
|
227
256
|
kt2 = kt1.dup
|
|
228
257
|
expect(kt2).to be_a(described_class)
|
|
229
258
|
expect(kt2.keytab_name).to eq(kt1.keytab_name)
|
|
@@ -234,8 +263,184 @@ RSpec.describe Kerberos::Krb5::Keytab do
|
|
|
234
263
|
end
|
|
235
264
|
|
|
236
265
|
it 'clone is an alias for dup' do
|
|
237
|
-
kt = described_class.new(@keytab_name)
|
|
266
|
+
kt = described_class.new(name: @keytab_name)
|
|
238
267
|
expect(kt.method(:clone)).to eq(kt.method(:dup))
|
|
239
268
|
end
|
|
240
269
|
end
|
|
270
|
+
|
|
271
|
+
describe '#have_content?' do
|
|
272
|
+
it 'returns true for a keytab with entries' do
|
|
273
|
+
kt = described_class.new(name: @keytab_name)
|
|
274
|
+
expect(kt.have_content?).to be true
|
|
275
|
+
kt.close
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it 'returns false for an empty keytab' do
|
|
279
|
+
empty_file = File.join(Dir.tmpdir, 'empty_test.keytab')
|
|
280
|
+
FileUtils.touch(empty_file)
|
|
281
|
+
kt = described_class.new(name: "FILE:#{empty_file}")
|
|
282
|
+
expect(kt.have_content?).to be false
|
|
283
|
+
kt.close
|
|
284
|
+
FileUtils.rm_f(empty_file)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
it 'raises when the context is closed' do
|
|
288
|
+
kt = described_class.new(name: @keytab_name)
|
|
289
|
+
kt.close
|
|
290
|
+
expect { kt.have_content? }.to raise_error(Kerberos::Krb5::Exception)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
describe '#add_entry' do
|
|
295
|
+
let(:add_kt_file) { File.join(Dir.tmpdir, 'add_test.keytab') }
|
|
296
|
+
let(:add_kt_name) { "FILE:#{add_kt_file}" }
|
|
297
|
+
|
|
298
|
+
after(:each) { FileUtils.rm_f(add_kt_file) }
|
|
299
|
+
|
|
300
|
+
it 'adds an entry that is retrievable via get_entry' do
|
|
301
|
+
kt = described_class.new(name: add_kt_name)
|
|
302
|
+
kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1)
|
|
303
|
+
|
|
304
|
+
entry = kt.get_entry("testuser1@#{@realm}")
|
|
305
|
+
expect(entry).to be_a(Kerberos::Krb5::Keytab::Entry)
|
|
306
|
+
expect(entry.principal).to eq("testuser1@#{@realm}")
|
|
307
|
+
expect(entry.vno).to eq(1)
|
|
308
|
+
# default enctype is aes256-cts-hmac-sha1-96 = 18
|
|
309
|
+
expect(entry.key).to eq(18)
|
|
310
|
+
kt.close
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it 'accepts an explicit enctype' do
|
|
314
|
+
kt = described_class.new(name: add_kt_name)
|
|
315
|
+
# aes128-cts-hmac-sha1-96 = 17
|
|
316
|
+
kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 2, enctype: 17)
|
|
317
|
+
|
|
318
|
+
entry = kt.get_entry("testuser1@#{@realm}", 2, 17)
|
|
319
|
+
expect(entry.vno).to eq(2)
|
|
320
|
+
expect(entry.key).to eq(17)
|
|
321
|
+
kt.close
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'can add multiple entries for the same principal' do
|
|
325
|
+
kt = described_class.new(name: add_kt_name)
|
|
326
|
+
kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1, enctype: 18)
|
|
327
|
+
kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1, enctype: 17)
|
|
328
|
+
|
|
329
|
+
entries = []
|
|
330
|
+
kt.each { |e| entries << e }
|
|
331
|
+
expect(entries.length).to eq(2)
|
|
332
|
+
kt.close
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it 'returns self for chaining' do
|
|
336
|
+
kt = described_class.new(name: add_kt_name)
|
|
337
|
+
result = kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret')
|
|
338
|
+
expect(result).to equal(kt)
|
|
339
|
+
kt.close
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
it 'raises ArgumentError when principal is missing' do
|
|
343
|
+
kt = described_class.new(name: add_kt_name)
|
|
344
|
+
expect { kt.add_entry(password: 'secret') }.to raise_error(ArgumentError)
|
|
345
|
+
kt.close
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it 'raises ArgumentError when password is missing' do
|
|
349
|
+
kt = described_class.new(name: add_kt_name)
|
|
350
|
+
expect { kt.add_entry(principal: "testuser1@#{@realm}") }.to raise_error(ArgumentError)
|
|
351
|
+
kt.close
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
it 'raises ArgumentError with no arguments' do
|
|
355
|
+
kt = described_class.new(name: add_kt_name)
|
|
356
|
+
expect { kt.add_entry }.to raise_error(ArgumentError)
|
|
357
|
+
kt.close
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it 'rejects unknown keywords' do
|
|
361
|
+
kt = described_class.new(name: add_kt_name)
|
|
362
|
+
expect {
|
|
363
|
+
kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', unknown: true)
|
|
364
|
+
}.to raise_error(ArgumentError, /unknown keyword/)
|
|
365
|
+
kt.close
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it 'raises when the keytab is closed' do
|
|
369
|
+
kt = described_class.new(name: add_kt_name)
|
|
370
|
+
kt.close
|
|
371
|
+
expect {
|
|
372
|
+
kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret')
|
|
373
|
+
}.to raise_error(Kerberos::Krb5::Exception)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
describe '#remove_entry' do
|
|
378
|
+
let(:rm_kt_file) { File.join(Dir.tmpdir, 'remove_test.keytab') }
|
|
379
|
+
let(:rm_kt_name) { "FILE:#{rm_kt_file}" }
|
|
380
|
+
|
|
381
|
+
before(:each) do
|
|
382
|
+
@rm_kt = described_class.new(name: rm_kt_name)
|
|
383
|
+
@rm_kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1, enctype: 18)
|
|
384
|
+
@rm_kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 2, enctype: 17)
|
|
385
|
+
@rm_kt.add_entry(principal: "testuser2@#{@realm}", password: 'secret', vno: 1, enctype: 18)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
after(:each) do
|
|
389
|
+
@rm_kt.close rescue nil
|
|
390
|
+
FileUtils.rm_f(rm_kt_file)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
it 'removes all entries for a principal when vno and enctype are defaulted' do
|
|
394
|
+
@rm_kt.remove_entry(principal: "testuser1@#{@realm}")
|
|
395
|
+
entries = []
|
|
396
|
+
@rm_kt.each { |e| entries << e }
|
|
397
|
+
principals = entries.map(&:principal)
|
|
398
|
+
expect(principals).not_to include("testuser1@#{@realm}")
|
|
399
|
+
expect(principals).to include("testuser2@#{@realm}")
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
it 'removes only the entry matching a specific vno' do
|
|
403
|
+
@rm_kt.remove_entry(principal: "testuser1@#{@realm}", vno: 1)
|
|
404
|
+
entry = @rm_kt.get_entry("testuser1@#{@realm}")
|
|
405
|
+
expect(entry.vno).to eq(2)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
it 'removes only the entry matching a specific enctype' do
|
|
409
|
+
@rm_kt.remove_entry(principal: "testuser1@#{@realm}", enctype: 17)
|
|
410
|
+
entry = @rm_kt.get_entry("testuser1@#{@realm}")
|
|
411
|
+
expect(entry.key).to eq(18)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
it 'returns self for chaining' do
|
|
415
|
+
result = @rm_kt.remove_entry(principal: "testuser2@#{@realm}")
|
|
416
|
+
expect(result).to equal(@rm_kt)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
it 'raises ArgumentError when principal is missing' do
|
|
420
|
+
expect { @rm_kt.remove_entry(vno: 1) }.to raise_error(ArgumentError)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
it 'raises ArgumentError with no arguments' do
|
|
424
|
+
expect { @rm_kt.remove_entry }.to raise_error(ArgumentError)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
it 'rejects unknown keywords' do
|
|
428
|
+
expect {
|
|
429
|
+
@rm_kt.remove_entry(principal: "testuser1@#{@realm}", unknown: true)
|
|
430
|
+
}.to raise_error(ArgumentError, /unknown keyword/)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
it 'raises when the keytab is closed' do
|
|
434
|
+
@rm_kt.close
|
|
435
|
+
expect {
|
|
436
|
+
@rm_kt.remove_entry(principal: "testuser1@#{@realm}")
|
|
437
|
+
}.to raise_error(Kerberos::Krb5::Exception)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
it 'raises for a non-existent entry' do
|
|
441
|
+
expect {
|
|
442
|
+
@rm_kt.remove_entry(principal: "nobody@#{@realm}")
|
|
443
|
+
}.to raise_error(Kerberos::Krb5::Keytab::Exception)
|
|
444
|
+
end
|
|
445
|
+
end
|
|
241
446
|
end
|
data/spec/krb5_spec.rb
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# spec/krb5_spec.rb
|
|
2
2
|
# RSpec tests for Kerberos::Krb5
|
|
3
3
|
|
|
4
|
-
require '
|
|
4
|
+
require 'spec_helper'
|
|
5
5
|
require 'open3'
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
unless File::ALT_SEPARATOR
|
|
8
|
+
require 'pty'
|
|
9
|
+
require 'expect'
|
|
10
|
+
end
|
|
8
11
|
|
|
9
12
|
RSpec.describe Kerberos::Krb5 do
|
|
10
13
|
before(:all) do
|
|
14
|
+
krb5_conf = RSpec.configuration.krb5_conf
|
|
11
15
|
@cache_found = true
|
|
12
16
|
Open3.popen3('klist') { |_, _, stderr| @cache_found = false unless stderr.gets.nil? }
|
|
13
|
-
@
|
|
14
|
-
@realm = IO.read(@krb5_conf).split("\n").grep(/default_realm/).first.split('=').last.lstrip.chomp
|
|
15
|
-
|
|
17
|
+
@realm = IO.read(krb5_conf).split("\n").grep(/default_realm/).first.split('=').last.lstrip.chomp
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
subject(:krb5) { described_class.new }
|
|
@@ -21,7 +23,7 @@ RSpec.describe Kerberos::Krb5 do
|
|
|
21
23
|
let(:service) { 'kadmin/admin' }
|
|
22
24
|
|
|
23
25
|
it 'has the correct version constant' do
|
|
24
|
-
expect(Kerberos::Krb5::VERSION).to eq('0.
|
|
26
|
+
expect(Kerberos::Krb5::VERSION).to eq('0.3.0')
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
it 'accepts a block and yields itself' do
|
|
@@ -29,6 +31,31 @@ RSpec.describe Kerberos::Krb5 do
|
|
|
29
31
|
described_class.new { |k| expect(k).to be_a(described_class) }
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
describe 'constructor' do
|
|
35
|
+
it 'accepts a context keyword argument' do
|
|
36
|
+
context = Kerberos::Krb5::Context.new
|
|
37
|
+
k = described_class.new(context: context)
|
|
38
|
+
expect(k).to be_a(described_class)
|
|
39
|
+
expect(k.get_default_realm).to eq(@realm)
|
|
40
|
+
k.close
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'raises TypeError if context is not a Kerberos::Krb5::Context' do
|
|
44
|
+
expect { described_class.new(context: 'bad') }.to raise_error(TypeError, /context must be/)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'raises an error if the context is closed' do
|
|
48
|
+
context = Kerberos::Krb5::Context.new
|
|
49
|
+
context.close
|
|
50
|
+
expect { described_class.new(context: context) }.to raise_error(Kerberos::Krb5::Exception, /context is closed/)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'rejects positional option hashes and unknown keywords' do
|
|
54
|
+
expect { described_class.new({context: nil}) }.to raise_error(ArgumentError)
|
|
55
|
+
expect { described_class.new(unknown: true) }.to raise_error(ArgumentError, /unknown keyword/)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
32
59
|
describe '#get_default_realm' do
|
|
33
60
|
it 'responds to get_default_realm' do
|
|
34
61
|
expect(krb5).to respond_to(:get_default_realm)
|
|
@@ -48,7 +75,7 @@ RSpec.describe Kerberos::Krb5 do
|
|
|
48
75
|
end
|
|
49
76
|
end
|
|
50
77
|
|
|
51
|
-
describe '#verify_init_creds' do
|
|
78
|
+
describe '#verify_init_creds', :kadm5 do
|
|
52
79
|
# Some KDC setups may not correctly set the initial password during
|
|
53
80
|
# entrypoint startup; enforce it here via the admin API so the test is
|
|
54
81
|
# deterministic.
|
|
@@ -66,57 +93,78 @@ RSpec.describe Kerberos::Krb5 do
|
|
|
66
93
|
expect(krb5).to respond_to(:verify_init_creds)
|
|
67
94
|
end
|
|
68
95
|
|
|
96
|
+
it 'requires get_init_creds_password arguments as keywords' do
|
|
97
|
+
expect { krb5.get_init_creds_password(user, 'changeme') }.to raise_error(ArgumentError)
|
|
98
|
+
expect {
|
|
99
|
+
krb5.get_init_creds_password(principal: user, password: 'changeme', unknown: true)
|
|
100
|
+
}.to raise_error(ArgumentError, /unknown keyword/)
|
|
101
|
+
end
|
|
102
|
+
|
|
69
103
|
it 'raises when no credentials have been acquired' do
|
|
70
104
|
expect { krb5.verify_init_creds }.to raise_error(Kerberos::Krb5::Exception)
|
|
71
105
|
end
|
|
72
106
|
|
|
73
107
|
it 'validates argument types' do
|
|
74
|
-
expect { krb5.verify_init_creds(true) }.to raise_error(TypeError)
|
|
75
|
-
expect { krb5.verify_init_creds(
|
|
76
|
-
expect { krb5.verify_init_creds(
|
|
108
|
+
expect { krb5.verify_init_creds(server: true) }.to raise_error(TypeError)
|
|
109
|
+
expect { krb5.verify_init_creds(keytab: true) }.to raise_error(TypeError)
|
|
110
|
+
expect { krb5.verify_init_creds(ccache: true) }.to raise_error(TypeError)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'accepts only documented keyword arguments' do
|
|
114
|
+
expect { krb5.verify_init_creds(nil) }.to raise_error(ArgumentError)
|
|
115
|
+
expect { krb5.verify_init_creds(unknown: true) }.to raise_error(ArgumentError, /unknown keyword/)
|
|
77
116
|
end
|
|
78
117
|
|
|
79
118
|
it 'verifies credentials obtained via password' do
|
|
80
|
-
krb5.get_init_creds_password(user, 'changeme')
|
|
119
|
+
krb5.get_init_creds_password(principal: user, password: 'changeme')
|
|
81
120
|
expect(krb5.verify_init_creds).to be true
|
|
82
121
|
end
|
|
83
122
|
|
|
84
123
|
it 'accepts a server principal string' do
|
|
85
|
-
krb5.get_init_creds_password(user, 'changeme')
|
|
86
|
-
expect(krb5.verify_init_creds("kadmin/admin@#{@realm}")).to be true
|
|
124
|
+
krb5.get_init_creds_password(principal: user, password: 'changeme')
|
|
125
|
+
expect(krb5.verify_init_creds(server: "kadmin/admin@#{@realm}")).to be true
|
|
87
126
|
end
|
|
88
127
|
|
|
89
128
|
it 'accepts a Keytab object' do
|
|
90
|
-
krb5.get_init_creds_password(user, 'changeme')
|
|
129
|
+
krb5.get_init_creds_password(principal: user, password: 'changeme')
|
|
91
130
|
kt = Kerberos::Krb5::Keytab.new
|
|
92
|
-
expect(krb5.verify_init_creds(
|
|
131
|
+
expect(krb5.verify_init_creds(keytab: kt)).to be true
|
|
93
132
|
end
|
|
94
133
|
|
|
95
134
|
it 'stores additional credentials in provided CredentialsCache' do
|
|
96
135
|
ccache = Kerberos::Krb5::CredentialsCache.new
|
|
97
|
-
krb5.get_init_creds_password(user, 'changeme')
|
|
98
|
-
expect(krb5.verify_init_creds(
|
|
136
|
+
krb5.get_init_creds_password(principal: user, password: 'changeme')
|
|
137
|
+
expect(krb5.verify_init_creds(ccache: ccache)).to be true
|
|
99
138
|
expect(ccache.primary_principal).to be_a(String)
|
|
100
139
|
expect(ccache.primary_principal).to include('@')
|
|
101
140
|
end
|
|
102
141
|
|
|
103
142
|
it 'provides authenticate! which acquires and verifies (Zanarotti mitigation)' do
|
|
104
143
|
expect(krb5).to respond_to(:authenticate!)
|
|
105
|
-
expect(krb5.authenticate!(user, 'changeme')).to be true
|
|
144
|
+
expect(krb5.authenticate!(principal: user, password: 'changeme')).to be true
|
|
106
145
|
expect(krb5.verify_init_creds).to be true
|
|
107
146
|
end
|
|
108
147
|
|
|
109
148
|
it 'accepts an optional service argument' do
|
|
110
|
-
expect {
|
|
149
|
+
expect {
|
|
150
|
+
krb5.authenticate!(principal: user, password: 'changeme', service: 'kadmin/changepw')
|
|
151
|
+
}.not_to raise_error
|
|
111
152
|
expect(krb5.verify_init_creds).to be true
|
|
112
153
|
end
|
|
113
154
|
|
|
114
155
|
it 'validates argument types for authenticate!' do
|
|
115
|
-
expect { krb5.authenticate!(true, true) }.to raise_error(TypeError)
|
|
156
|
+
expect { krb5.authenticate!(principal: true, password: true) }.to raise_error(TypeError)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'requires authenticate! arguments as keywords' do
|
|
160
|
+
expect { krb5.authenticate!(user, 'changeme') }.to raise_error(ArgumentError)
|
|
161
|
+
expect {
|
|
162
|
+
krb5.authenticate!(principal: user, password: 'changeme', unknown: true)
|
|
163
|
+
}.to raise_error(ArgumentError, /unknown keyword/)
|
|
116
164
|
end
|
|
117
165
|
end
|
|
118
166
|
|
|
119
|
-
describe '#change_password' do
|
|
167
|
+
describe '#change_password', :kadm5 do
|
|
120
168
|
before do
|
|
121
169
|
# Ensure testuser1 has a known password before each test.
|
|
122
170
|
Kerberos::Kadm5.new(
|
|
@@ -152,16 +200,16 @@ RSpec.describe Kerberos::Krb5 do
|
|
|
152
200
|
end
|
|
153
201
|
|
|
154
202
|
it 'changes the password successfully' do
|
|
155
|
-
krb5.get_init_creds_password(user, 'changeme')
|
|
203
|
+
krb5.get_init_creds_password(principal: user, password: 'changeme')
|
|
156
204
|
expect(krb5.change_password('changeme', 'Newpass99!')).to be true
|
|
157
205
|
# Verify we can authenticate with the new password
|
|
158
206
|
krb5_check = described_class.new
|
|
159
|
-
expect(krb5_check.get_init_creds_password(user, 'Newpass99!')).to be true
|
|
207
|
+
expect(krb5_check.get_init_creds_password(principal: user, password: 'Newpass99!')).to be true
|
|
160
208
|
krb5_check.close
|
|
161
209
|
end
|
|
162
210
|
|
|
163
211
|
it 'raises with a meaningful message when the old password is wrong' do
|
|
164
|
-
krb5.get_init_creds_password(user, 'changeme')
|
|
212
|
+
krb5.get_init_creds_password(principal: user, password: 'changeme')
|
|
165
213
|
expect {
|
|
166
214
|
krb5.change_password('wrongpass', 'Newpass99!')
|
|
167
215
|
}.to raise_error(Kerberos::Krb5::Exception, /krb5_(get_init_creds_password|change_password)/)
|
|
@@ -178,7 +226,7 @@ RSpec.describe Kerberos::Krb5 do
|
|
|
178
226
|
let(:compliant_pw) { 'Changeme1!' }
|
|
179
227
|
|
|
180
228
|
it 'raises an exception with the KDC rejection reason' do
|
|
181
|
-
krb5.get_init_creds_password(policy_user, compliant_pw)
|
|
229
|
+
krb5.get_init_creds_password(principal: policy_user, password: compliant_pw)
|
|
182
230
|
expect {
|
|
183
231
|
krb5.change_password(compliant_pw, 'a')
|
|
184
232
|
}.to raise_error(Kerberos::Krb5::Exception, /krb5_change_password/)
|
|
@@ -186,7 +234,7 @@ RSpec.describe Kerberos::Krb5 do
|
|
|
186
234
|
end
|
|
187
235
|
end
|
|
188
236
|
|
|
189
|
-
describe '#get_init_creds_keytab' do
|
|
237
|
+
describe '#get_init_creds_keytab', :unix do
|
|
190
238
|
before(:each) do
|
|
191
239
|
@kt_file = File.join(Dir.tmpdir, "test_get_init_creds_#{Process.pid}_#{rand(10000)}.keytab")
|
|
192
240
|
|
|
@@ -206,18 +254,83 @@ RSpec.describe Kerberos::Krb5 do
|
|
|
206
254
|
expect(krb5).to respond_to(:get_init_creds_keytab)
|
|
207
255
|
end
|
|
208
256
|
|
|
257
|
+
it 'accepts only documented keyword arguments' do
|
|
258
|
+
expect { krb5.get_init_creds_keytab(user) }.to raise_error(ArgumentError)
|
|
259
|
+
expect { krb5.get_init_creds_keytab(unknown: true) }.to raise_error(ArgumentError, /unknown keyword/)
|
|
260
|
+
end
|
|
261
|
+
|
|
209
262
|
it 'acquires credentials for a principal from a supplied keytab file' do
|
|
210
263
|
kt_name = "FILE:#{@kt_file}"
|
|
211
|
-
expect { krb5.get_init_creds_keytab(user, kt_name) }.not_to raise_error
|
|
264
|
+
expect { krb5.get_init_creds_keytab(principal: user, keytab: kt_name) }.not_to raise_error
|
|
212
265
|
expect(krb5.verify_init_creds).to be true
|
|
213
266
|
end
|
|
214
267
|
|
|
215
268
|
it 'accepts a CredentialsCache to receive credentials' do
|
|
216
269
|
ccache = Kerberos::Krb5::CredentialsCache.new
|
|
217
270
|
kt_name = "FILE:#{@kt_file}"
|
|
218
|
-
expect { krb5.get_init_creds_keytab(user, kt_name,
|
|
271
|
+
expect { krb5.get_init_creds_keytab(principal: user, keytab: kt_name, ccache: ccache) }.not_to raise_error
|
|
219
272
|
expect(ccache.primary_principal).to be_a(String)
|
|
220
273
|
expect(ccache.primary_principal).to include('@')
|
|
221
274
|
end
|
|
222
275
|
end
|
|
276
|
+
|
|
277
|
+
describe '.thread_safe?' do
|
|
278
|
+
it 'returns a boolean' do
|
|
279
|
+
result = described_class.thread_safe?
|
|
280
|
+
expect([true, false]).to include(result)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it 'is callable without an instance' do
|
|
284
|
+
expect(described_class).to respond_to(:thread_safe?)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
describe '#get_host_realm' do
|
|
289
|
+
it 'responds to get_host_realm' do
|
|
290
|
+
expect(krb5).to respond_to(:get_host_realm)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it 'returns an array of realm strings' do
|
|
294
|
+
result = krb5.get_host_realm('localhost')
|
|
295
|
+
expect(result).to be_a(Array)
|
|
296
|
+
result.each { |r| expect(r).to be_a(String) }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it 'returns a non-empty array for a known hostname' do
|
|
300
|
+
result = krb5.get_host_realm('localhost')
|
|
301
|
+
expect(result).not_to be_empty
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
it 'raises TypeError for non-string argument' do
|
|
305
|
+
expect { krb5.get_host_realm(123) }.to raise_error(TypeError)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'raises ArgumentError when called without arguments' do
|
|
309
|
+
expect { krb5.get_host_realm }.to raise_error(ArgumentError)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
describe '#expand_hostname' do
|
|
314
|
+
it 'responds to expand_hostname' do
|
|
315
|
+
expect(krb5).to respond_to(:expand_hostname)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
it 'returns a string' do
|
|
319
|
+
result = krb5.expand_hostname('localhost')
|
|
320
|
+
expect(result).to be_a(String)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it 'returns a non-empty string' do
|
|
324
|
+
result = krb5.expand_hostname('localhost')
|
|
325
|
+
expect(result.size).to be > 0
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it 'raises TypeError for non-string argument' do
|
|
329
|
+
expect { krb5.expand_hostname(123) }.to raise_error(TypeError)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
it 'raises ArgumentError when called without arguments' do
|
|
333
|
+
expect { krb5.expand_hostname }.to raise_error(ArgumentError)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
223
336
|
end
|